package com.sean.community.util;

import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.buf.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

/**
 * 基于前缀树实现的敏感词过滤器
 */
@Component
public class SensitiveFilter {
    // 日志
    private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
    // 用来替换的符号
    private static final String REPLACEMENT = "***";
    // 根节点
    private TrieNode root = new TrieNode();

    /**
     * 初始化前缀树
     */
    @PostConstruct          // 当容器实例化 Bean 之后，在调用构造器之后，该方法自动被调用
    public void init(){
        try(
                // 读取敏感文件
                // 使用类加载器从 classes 目录下加载资源文件
                InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
                // InputStreamReader 字节流到字符流的桥接器
                // BufferedReader 从字符输入流中读取文本并缓冲字符
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        ) {
            String keyword;
            while((keyword = bufferedReader.readLine()) != null){
                // 添加到前缀树
                this.addKeyword(keyword);
            }
        } catch (IOException e) {
            logger.error("敏感词文件加载失败！" + e.getMessage());
        }
    }

    /**
     * 将一个敏感词添加到前缀树中
     * @param keyword 敏感词
     */
    private void addKeyword(String keyword){
        TrieNode p = this.root;
        for(int i = 0; i < keyword.length(); i++){
            char c = keyword.charAt(i);
            if(p.getChildNode(c) == null){
                p.addChildNode(c, new TrieNode());
            }
            p = p.getChildNode(c);
        }
        p.setKeyWordEnd(true);
    }

    /**
     * 过滤敏感词
     * @param text 待过滤的文本
     * @return 过滤好的文本
     */
    public String filter(String text){
        if(StringUtils.isBlank(text)){
            return null;
        }
        TrieNode p = this.root;
        int start = 0;
        int end = 0;
        StringBuilder stringBuilder = new StringBuilder();
        while(end < text.length()){
            char c = text.charAt(end);
            // 跳过符号（防敏感词被绕过）
            // ，敏，感，词，
            if(isSymbol(c)){
                // 敏感词前的特殊符号，不做处理
                if(p == this.root){
                    stringBuilder.append(c);
                    start++;
                }
                // 无论符号在开头还是之间，end 指针都右移一步
                end++;
                continue;
            }
            // 前缀树匹配
            p = p.getChildNode(c);
            if(p == null){  // 以 start 开头的字符表示敏感词
                stringBuilder.append(text.charAt(start));
                start++;
                end = start;
                p = this.root;
            }else if(p.isKeyWordEnd()){
                // 发现敏感词， start 到 end 需要被替换
                stringBuilder.append(REPLACEMENT);
                end ++;
                start = end;
                p = this.root;
            }else{
                // 指针处于敏感词中间
                end++;
            }
        }
        // 将最后几个字符计入结果
        stringBuilder.append(text.substring(start));
        return stringBuilder.toString();
    }

    /**
     * 判断是否为符号
     * @param c 字符
     * @return 是 / 否
     */
    private boolean isSymbol(Character c){
        // isAsciiAlphanumeric 判断是否为普通字符
        // [0x2E80, 0x9FFF] 东亚文字范围
        return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }


    /**
     * 前缀树节点
     */
    private class TrieNode{
        // 关键词结束标识
        private boolean isKeyWordEnd = false;
        // 子节点
        private Map<Character, TrieNode> childNode = new HashMap<>();

        public boolean isKeyWordEnd() {
            return isKeyWordEnd;
        }

        public void setKeyWordEnd(boolean keyWordEnd) {
            isKeyWordEnd = keyWordEnd;
        }

        public void addChildNode(Character c, TrieNode node){
            childNode.put(c, node);
        }

        public TrieNode getChildNode(Character c){
            return childNode.get(c);
        }
    }
}

